﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.ComponentModel;

namespace AutoNotifyPropertyChanged
{
    public static class Modifier
    {
        /// <summary>
        /// Cached modified types
        /// </summary>
        private static Dictionary<Type, Type> modifieds = new Dictionary<Type, Type>();
        /// <summary>
        /// Cached copy fields
        /// </summary>
        private static Dictionary<Type, FieldInfo[]> fieldinfos = new Dictionary<Type, FieldInfo[]>();
        /// <summary>
        /// Same assembly
        /// </summary>
        private static AssemblyBuilder assemblyBuilder;
        /// <summary>
        /// Same module
        /// </summary>
        private static ModuleBuilder moduleBuilder;
        /// <summary>
        /// Assembly Instance
        /// </summary>
        private static AssemblyBuilder AssemblyBuilder
        {
            get
            {
                if (assemblyBuilder == null)
                {
                    assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("ModifiedAssembly"), AssemblyBuilderAccess.Run);
                }
                return assemblyBuilder;
            }
        }
        /// <summary>
        /// Module Instance
        /// </summary>
        private static ModuleBuilder ModuleBuilder
        {
            get
            {
                if (moduleBuilder == null)
                {
                    moduleBuilder = AssemblyBuilder.DefineDynamicModule("ModifiedModule");
                }
                return moduleBuilder;
            }
        }

        private static object Create<T>()
        {
            // Get base type
            Type type = typeof(T);
            // Modified type
            Type modified;
            // Load from cache or create a new modified type
            modified = LoadOrCreate(type);
            // Create instance from modified type
            return Activator.CreateInstance(modified);
        }

        private static Type LoadOrCreate(Type type)
        {
            Type modified;
            // Try load
            if (!modifieds.TryGetValue(type, out modified))
            {
                // Create modified type
                TypeBuilder typeBuilder = ModuleBuilder.DefineType(type.Name + "_Modified", TypeAttributes.Public, type);
                // Get raise method
                MethodInfo onPropertyChangedMethodInfo = typeof(ModelBase).GetMethod("OnPropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder, new Type[1] { typeof(string) }, null);
                // Get all properties
                type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy).
                    // Filter virtual and public or protected
                Where(p => p.GetSetMethod().IsVirtual && (p.GetSetMethod().IsPublic || p.GetSetMethod().IsFamily)).ToList().
                    // Override all set methods
                ForEach((property) =>
                {
                    // Set method
                    MethodInfo setMethodInfo = property.GetSetMethod();
                    // Create new method
                    MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(setMethodInfo.Name, setMethodInfo.Attributes, setMethodInfo.ReturnType, setMethodInfo.GetParameters().Select(t => t.ParameterType).ToArray());
                    // Override set method
                    typeBuilder.DefineMethodOverride(setMethodBuilder, setMethodInfo);

                    // New method's ilgenerator
                    ILGenerator setMethodGenerator = setMethodBuilder.GetILGenerator();
                    // Get argument_0 "this"
                    setMethodGenerator.Emit(OpCodes.Ldarg_0);
                    // Get argument_1 "string parameterName"
                    setMethodGenerator.Emit(OpCodes.Ldarg_1);
                    // Call "base.set " method
                    setMethodGenerator.EmitCall(OpCodes.Call, setMethodInfo, null);
                    // Get argument_0 "this"
                    setMethodGenerator.Emit(OpCodes.Ldarg_0);
                    // Set argument_1 "string property.Name"
                    setMethodGenerator.Emit(OpCodes.Ldstr, property.Name);
                    // Call this.OnPropertyChanged(property.Name);
                    setMethodGenerator.EmitCall(OpCodes.Call, onPropertyChangedMethodInfo, null);
                    setMethodGenerator.Emit(OpCodes.Ret);

                    /*
                     * public override void set_PropertyName(ParameterType value)
                     * {
                     *      base.set_PropertyName(value);
                     *      OnPropertyChanged(PropertyName);
                     * }
                     * */
                });
                // Create modified type
                modified = typeBuilder.CreateType();
                // Cache
                modifieds.Add(type, modified);
            }
            return modified;
        }

        public static T Modify<T>(this T instance, bool copyValues = false) where T : ModelBase
        {
            object value = Modifier.Create<T>();
            // Copy values
            if (copyValues)
            {
                // Create value
                FieldInfo[] fields;
                // Get fields
                if (!fieldinfos.TryGetValue(value.GetType(), out fields))
                {
                    // Get all fields
                    fields = typeof(T).GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
                    // Cache
                    fieldinfos.Add(value.GetType(), fields);
                }

                for (int i = 0; i < fields.Length; i++)
                {
                    // Copy values from instance to new instance
                    fields[i].SetValue(value, fields[i].GetValue(instance));
                }
            }

            // return new instance
            return (T)value;
        }
    }
}
